(function($) {
    var version = '2.2.0';
    var optionOverrides = {};
    var defaults = {
        exclude: [],
        excludeWithin: [],
        offset: 0,

        // one of 'top' or 'left'
        direction: 'top',

        // if set, bind click events through delegation
        //  supported since jQuery 1.4.2
        delegateSelector: null,

        // jQuery set of elements you wish to scroll (for $.smoothScroll).
        //  if null (default), $('html, body').firstScrollable() is used.
        scrollElement: null,

        // only use if you want to override default behavior
        scrollTarget: null,

        // automatically focus the target element after scrolling to it
        autoFocus: false,

        // fn(opts) function to be called before scrolling occurs.
        // `this` is the element(s) being scrolled
        beforeScroll: function() {},

        // fn(opts) function to be called after scrolling occurs.
        // `this` is the triggering element
        afterScroll: function() {},

        // easing name. jQuery comes with "swing" and "linear." For others, you'll need an easing plugin
        // from jQuery UI or elsewhere
        easing: 'swing',

        // speed can be a number or 'auto'
        // if 'auto', the speed will be calculated based on the formula:
        // (current scroll position - target scroll position) / autoCoeffic
        speed: 400,

        // coefficient for "auto" speed
        autoCoefficient: 2,

        // $.fn.smoothScroll only: whether to prevent the default click action
        preventDefault: true
    };

    var getScrollable = function(opts) {
        var scrollable = [];
        var scrolled = false;
        var dir = opts.dir && opts.dir === 'left' ? 'scrollLeft' : 'scrollTop';

        this.each(function() {
            var el = $(this);

            if (this === document || this === window) {
                return;
            }

            if (document.scrollingElement && (this === document.documentElement || this === document.body)) {
                scrollable.push(document.scrollingElement);

                return false;
            }

            if (el[dir]() > 0) {
                scrollable.push(this);
            } else {
                // if scroll(Top|Left) === 0, nudge the element 1px and see if it moves
                el[dir](1);
                scrolled = el[dir]() > 0;

                if (scrolled) {
                    scrollable.push(this);
                }
                // then put it back, of course
                el[dir](0);
            }
        });

        if (!scrollable.length) {
            this.each(function() {
                // If no scrollable elements and <html> has scroll-behavior:smooth because
                // "When this property is specified on the root element, it applies to the viewport instead."
                // and "The scroll-behavior property of the … body element is *not* propagated to the viewport."
                // → https://drafts.csswg.org/cssom-view/#propdef-scroll-behavior
                if (this === document.documentElement && $(this).css('scrollBehavior') === 'smooth') {
                    scrollable = [this];
                }

                // If still no scrollable elements, fall back to <body>,
                // if it's in the jQuery collection
                // (doing this because Safari sets scrollTop async,
                // so can't set it to 1 and immediately get the value.)
                if (!scrollable.length && this.nodeName === 'BODY') {
                    scrollable = [this];
                }
            });
        }

        // Use the first scrollable element if we're calling firstScrollable()
        if (opts.el === 'first' && scrollable.length > 1) {
            scrollable = [scrollable[0]];
        }

        return scrollable;
    };

    var rRelative = /^([\-\+]=)(\d+)/;

    $.fn.extend({
        scrollable: function(dir) {
            var scrl = getScrollable.call(this, { dir: dir });

            return this.pushStack(scrl);
        },
        firstScrollable: function(dir) {
            var scrl = getScrollable.call(this, { el: 'first', dir: dir });

            return this.pushStack(scrl);
        },

        smoothScroll: function(options, extra) {
            options = options || {};

            if (options === 'options') {
                if (!extra) {
                    return this.first().data('ssOpts');
                }

                return this.each(function() {
                    var $this = $(this);
                    var opts = $.extend($this.data('ssOpts') || {}, extra);

                    $(this).data('ssOpts', opts);
                });
            }

            var opts = $.extend({}, $.fn.smoothScroll.defaults, options);

            var clickHandler = function(event) {
                var escapeSelector = function(str) {
                    return str.replace(/(:|\.|\/)/g, '\\$1');
                };

                var link = this;
                var $link = $(this);
                var thisOpts = $.extend({}, opts, $link.data('ssOpts') || {});
                var exclude = opts.exclude;
                var excludeWithin = thisOpts.excludeWithin;
                var elCounter = 0;
                var ewlCounter = 0;
                var include = true;
                var clickOpts = {};
                var locationPath = $.smoothScroll.filterPath(location.pathname);
                var linkPath = $.smoothScroll.filterPath(link.pathname);
                var hostMatch = location.hostname === link.hostname || !link.hostname;
                var pathMatch = thisOpts.scrollTarget || (linkPath === locationPath);
                var thisHash = escapeSelector(link.hash);

                if (thisHash && !$(thisHash).length) {
                    include = false;
                }

                if (!thisOpts.scrollTarget && (!hostMatch || !pathMatch || !thisHash)) {
                    include = false;
                } else {
                    while (include && elCounter < exclude.length) {
                        if ($link.is(escapeSelector(exclude[elCounter++]))) {
                            include = false;
                        }
                    }

                    while (include && ewlCounter < excludeWithin.length) {
                        if ($link.closest(excludeWithin[ewlCounter++]).length) {
                            include = false;
                        }
                    }
                }

                if (include) {
                    if (thisOpts.preventDefault) {
                        event.preventDefault();
                    }

                    $.extend(clickOpts, thisOpts, {
                        scrollTarget: thisOpts.scrollTarget || thisHash,
                        link: link
                    });

                    $.smoothScroll(clickOpts);
                }
            };

            if (options.delegateSelector !== null) {
                this
                    .off('click.smoothscroll', options.delegateSelector)
                    .on('click.smoothscroll', options.delegateSelector, clickHandler);
            } else {
                this
                    .off('click.smoothscroll')
                    .on('click.smoothscroll', clickHandler);
            }

            return this;
        }
    });

    var getExplicitOffset = function(val) {
        var explicit = { relative: '' };
        var parts = typeof val === 'string' && rRelative.exec(val);

        if (typeof val === 'number') {
            explicit.px = val;
        } else if (parts) {
            explicit.relative = parts[1];
            explicit.px = parseFloat(parts[2]) || 0;
        }

        return explicit;
    };

    var onAfterScroll = function(opts) {
        var $tgt = $(opts.scrollTarget);

        if (opts.autoFocus && $tgt.length) {
            $tgt[0].focus();

            if (!$tgt.is(document.activeElement)) {
                $tgt.prop({ tabIndex: -1 });
                $tgt[0].focus();
            }
        }

        opts.afterScroll.call(opts.link, opts);
    };

    $.smoothScroll = function(options, px) {
        if (options === 'options' && typeof px === 'object') {
            return $.extend(optionOverrides, px);
        }
        var opts, $scroller, speed, delta;
        var explicitOffset = getExplicitOffset(options);
        var scrollTargetOffset = {};
        var scrollerOffset = 0;
        var offPos = 'offset';
        var scrollDir = 'scrollTop';
        var aniProps = {};
        var aniOpts = {};

        if (explicitOffset.px) {
            opts = $.extend({ link: null }, $.fn.smoothScroll.defaults, optionOverrides);
        } else {
            opts = $.extend({ link: null }, $.fn.smoothScroll.defaults, options || {}, optionOverrides);

            if (opts.scrollElement) {
                offPos = 'position';

                if (opts.scrollElement.css('position') === 'static') {
                    opts.scrollElement.css('position', 'relative');
                }
            }

            if (px) {
                explicitOffset = getExplicitOffset(px);
            }
        }

        scrollDir = opts.direction === 'left' ? 'scrollLeft' : scrollDir;

        if (opts.scrollElement) {
            $scroller = opts.scrollElement;

            if (!explicitOffset.px && !(/^(?:HTML|BODY)$/).test($scroller[0].nodeName)) {
                scrollerOffset = $scroller[scrollDir]();
            }
        } else {
            $scroller = $('html, body').firstScrollable(opts.direction);
        }

        // beforeScroll callback function must fire before calculating offset
        opts.beforeScroll.call($scroller, opts);

        scrollTargetOffset = explicitOffset.px ? explicitOffset : {
            relative: '',
            px: ($(opts.scrollTarget)[offPos]() && $(opts.scrollTarget)[offPos]()[opts.direction]) || 0
        };

        aniProps[scrollDir] = scrollTargetOffset.relative + (scrollTargetOffset.px + scrollerOffset + opts.offset);

        speed = opts.speed;

        // automatically calculate the speed of the scroll based on distance / coefficient
        if (speed === 'auto') {

            // $scroller[scrollDir]() is position before scroll, aniProps[scrollDir] is position after
            // When delta is greater, speed will be greater.
            delta = Math.abs(aniProps[scrollDir] - $scroller[scrollDir]());

            // Divide the delta by the coefficient
            speed = delta / opts.autoCoefficient;
        }

        aniOpts = {
            duration: speed,
            easing: opts.easing,
            complete: function() {
                onAfterScroll(opts);
            }
        };

        if (opts.step) {
            aniOpts.step = opts.step;
        }

        if ($scroller.length) {
            $scroller.stop().animate(aniProps, aniOpts);
        } else {
            onAfterScroll(opts);
        }
    };

    $.smoothScroll.version = version;
    $.smoothScroll.filterPath = function(string) {
        string = string || '';

        return string
            .replace(/^\//, '')
            .replace(/(?:index|default).[a-zA-Z]{3,4}$/, '')
            .replace(/\/$/, '');
    };

    // default options
    $.fn.smoothScroll.defaults = defaults;
})(jQuery);